package utils;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

import utils.math_utils.MathUtils;
import executives.thread_managers.SpikeProducer;

/**
 * The state camera is the primary means by which spike trains are translated
 * into state arrays and then collected into a state matrix. This is how the 
 * output of the LSM is translated into a form which is comprehensible to 
 * standard computers. As a reminder, LSMs perform real-time computations
 * on continuous inputs, but for the output of these computations to be 
 * comprehensible by a computer which performs static computations on discrete
 * inputs a translation must be performed. State Camera performs this
 * translation.
 *  
 * @author zach
 *
 */
public class StateCamera {

	/** 
	 * The encoding scheme: what should a neuron's "state" be at a given time
	 * based on its spike history?
	 * 
	 * @author zach
	 *
	 */
	public enum CameraType {
		Bin, Bin_Binary, Bin_Bit, Exp, Exp_Conv;
	}
	
	/** The encoding scheme. */
	private final CameraType cType;
	
	private int epochs;
	private final int numFrames;
	private int fIndex;
	private int sIndex;
	private double [][] capturedStates;
	private boolean [][] frameStack;
	private boolean [] frame;

	private boolean parallel;
	
	public StateCamera(CameraType cType, double timeStep, double exposureTime) {
		consistencyCheck(timeStep, exposureTime);
		this.cType = cType;
		numFrames =  (int) (exposureTime/timeStep);
		sIndex = 0;
	}
	
	public void readyFilm(int epochs, int numNeurons) {
		this.epochs = epochs;
		capturedStates = new double [epochs][numNeurons];
		frame = new boolean [numNeurons];
		frameStack = new boolean [numFrames][numNeurons];
		fIndex = 0;
		sIndex = 0;
	}
	
	public void spikeEvent(int index) {
		frame[index] = true;
		//System.out.println("I happen");
	}
	
	public void directSnapshot(){
		int n = frame.length;
		for(int i = 0; i < n; i++) {
			capturedStates[sIndex][i] = frame[i] ? 1.0 : 0.0;
		}
		sIndex++;
		frame = new boolean[frame.length];
	}
	
	public boolean collectFrame() {
		int n = frame.length;
		frameStack[fIndex] = Arrays.copyOf(frame, n);
		fIndex++;
		if(fIndex == numFrames){
			return collapseFrameStack();
		} else {
			frame = new boolean[n];
			if(parallel)
				SpikeProducer.syncLatch.countDown();
			return false;
		}
	}
	
	public boolean collapseFrameStack () {
		fIndex = 0;
		frame = new boolean[frame.length];
		if(sIndex >= epochs) {
			return true;
		} else {
			switch(cType) {
				case Bin:			capturedStates[sIndex] = binCollapse(); break;
				case Bin_Binary:	capturedStates[sIndex] = binBinary(); break;
				case Bin_Bit:		capturedStates[sIndex] = binBitCollapse(); break;
				case Exp:			/*TODO*/ break;
				case Exp_Conv:		/*TODO*/ break;
				default: 	System.err.println("Unrecognized Camera Type." +
						" Frame collapse failure... exiting..."); System.exit(1);
			}
			frameStack = new boolean [numFrames][frame.length];
			sIndex++;
			if(parallel)
				SpikeProducer.syncLatch.countDown();
			return false;
		}
	}
	
	/**
	 * 
	 * @param frames
	 * @return
	 */
	public static double [] collapseFramStack(double [][] frames) {
		MathUtils.checkUniformCols(frames);
		double [] cs = new double[frames[0].length];
		
		for(int i = 0 , n = frames.length; i < n; i++) {
			MathUtils.arrayAddInPlace1D(cs, frames[i]);
		}
		
		return cs;
	}
	
	/**
	 * 
	 * @return
	 */
	public double [] binBitCollapse() {
		int n = frame.length;
		double [] state = new double[n];
		for(int j = 0; j < n; j++) {
			String val = "";
			for (int i = 0; i < numFrames; i++) {
				if(frameStack[i][j]) {
					val.concat("1");
				} else {
					val.concat("0");
				}
			}
			state[j] = Integer.parseInt(val, 2);			
		}
		return state;
	}
	
	public double [] binBinary() {
		int n = frame.length;
		double [] state = new double[n];
		for(int j = 0; j < n; j++) {
			for (int i = 0; i < numFrames; i++) {
				if(frameStack[i][j]) {
					state[j] = 1;
					break;
				}
			}	
		}
		//System.out.println(Arrays.toString(state));
		return state;
	}
	
	
	/**
	 * 
	 * @return
	 */
	public double [] binCollapse() {
		int n = frame.length;
		double [] state = new double[n];
		int bCount;
		for(int j = 0; j < n; j++) {
			bCount = 0;
			for (int i = 0; i < numFrames; i++) {
				if(frameStack[i][j]) {
					bCount++;
				}
			}
			state[j] = bCount;			
		}
		return state;
	}
	
	
	private static void consistencyCheck (double timeStep, double exposureTime)
			throws IllegalArgumentException {
		if(exposureTime < timeStep) {
			throw new IllegalArgumentException("Exposure Time must be" +
					" greater than or equal to time step time.");
		}
	}
	
	public void printStatesToFile(String filename) {
		try {
		FileWriter fw = new FileWriter(filename);
		PrintWriter pw = new PrintWriter(fw);
		
		for(int i = 0, n = capturedStates.length; i < n; i++) {
			for(int j = 0; j < capturedStates[0].length; j++) {
				pw.print(capturedStates[i][j] + ", ");
			}
			pw.println();
		}
		
		pw.close();
		
		} catch(IOException e) {
			e.printStackTrace();
		}
	}

	public double[][] getCapturedStates() {
		return capturedStates;
	}

	public void setCapturedStates(double[][] capturedStates) {
		this.capturedStates = capturedStates;
	}

	public boolean[] getFrame() {
		return frame;
	}

	public void setFrame(boolean[] frame) {
		this.frame = frame;
	}

	public boolean isParallel() {
		return parallel;
	}

	public void setParallel(boolean parallel) {
		this.parallel = parallel;
	}
	
	public boolean [][] getFrameStack(){
		return frameStack;
	}
	
}
